package mmc.cell;

import static org.lwjgl.opengl.GL11.GL_FLOAT;
import static org.lwjgl.opengl.GL11.GL_QUADS;
import static org.lwjgl.opengl.GL11.GL_UNSIGNED_INT;
import static org.lwjgl.opengl.GL11.glDrawElements;
import static org.lwjgl.opengl.GL11.glNormalPointer;
import static org.lwjgl.opengl.GL11.glTexCoordPointer;
import static org.lwjgl.opengl.GL11.glVertexPointer;
import static org.lwjgl.opengl.GL15.GL_ARRAY_BUFFER;
import static org.lwjgl.opengl.GL15.GL_ELEMENT_ARRAY_BUFFER;
import static org.lwjgl.opengl.GL15.GL_STATIC_DRAW;
import static org.lwjgl.opengl.GL15.GL_WRITE_ONLY;
import static org.lwjgl.opengl.GL15.glBindBuffer;
import static org.lwjgl.opengl.GL15.glBufferData;
import static org.lwjgl.opengl.GL15.glDeleteBuffers;
import static org.lwjgl.opengl.GL15.glGenBuffers;
import static org.lwjgl.opengl.GL15.glMapBuffer;
import static org.lwjgl.opengl.GL15.glUnmapBuffer;

import java.nio.ByteBuffer;

import mmc.Constants;
import mmc.EngineObject;
import mmc.Player;

public class ClientCell{

	CellPosition pos;
	byte[] block;
	int solid;
	
	int[] arbVertex = new int[6];
	int[] arbIndex = new int[6];
	int[] visibleSurfaces = null;

	int notRenderedFrames = 0;
	boolean cellContentChanged = true;
	boolean calculateVisibleSurfacesOnNextUpdate = false;
	boolean loadOnNextUpdate = false;
	boolean[] loadRequested = new boolean[]{false,false,false,false,false,false};
	boolean visibilityImpossible = false;
	boolean cellVisibilityTestRequested = true;
	
	static boolean[][][] visibilityTest = new boolean[Constants.CELL_SIZE][Constants.CELL_SIZE][Constants.CELL_SIZE];
	static final int vertexFloatCount = 4+3+2;
	
	public ClientCell(ServerCell cell){
		pos = cell.pos;
		block = cell.block;
		solid = cell.solid;
	}
	
	public ClientCell(int x, int y, int z, byte[] content) {
		this(new CellPosition(x,y,z), content);
	}
	
	public ClientCell(CellPosition pos, byte[] content) {
		this.pos = pos;
		block = content;
		solid = ServerCell.CELL_NOT_COMPRESSED;
	}
	
	public ClientCell(int x, int y, int z, int solid) {
		this(new CellPosition(x,y,z), solid);
	}
	
	public ClientCell(CellPosition pos, int solid){
		this.pos = pos;
		block = null;
		this.solid = solid;
	}
	
	public boolean isCompressed(){
		return solid != ServerCell.CELL_NOT_COMPRESSED;
	}
	
	private boolean translucent(byte code){
		return Block.isTranslucent(code);
	}
	
	private boolean cellTranslucent(){
		return solid != ServerCell.CELL_NOT_COMPRESSED && Block.isTranslucent((byte)solid);
	}
	
	private boolean isTranslucencyImpossible(){
		return solid != ServerCell.CELL_NOT_COMPRESSED && Block.isSolid((byte)solid);
	}
	
	public boolean isTranslucent(int x, int y, int z){
		return cellTranslucent() || translucent(getValue(x,y,z));
	}
	
	public boolean contains(int x, int y, int z){
		return x>pos.x && x<pos.x+Constants.CELL_SIZE &&
			   y>pos.y && y<pos.y+Constants.CELL_SIZE &&
			   z>pos.z && z<pos.z+Constants.CELL_SIZE;
	}
	
	private byte getValue(int x, int y, int z){
		if(solid!=ServerCell.CELL_NOT_COMPRESSED)
			return (byte)solid;
		else
			return block[x*Constants.CELL_SIZE2+y*Constants.CELL_SIZE+z];
	}
	
	public void calculateVisibleSurfacesCount(){
		//System.out.println("calculating vis-surf for "+this);
		
		if(visibleSurfaces == null){
			visibleSurfaces = new int[6];
		}
		
		if(cellTranslucent()){
			for(int i=0;i<6;i++)
				visibleSurfaces[i] = 0;
			return;
		}
		
		if(isCompressed()){
			for(int d = 0; d < 3; d++){
				for(int o = 0; o<2; o++){
					int id = d*2+o;
					int or = (o*2-1)*Constants.CELL_SIZE;
					
					int cx = d==0?pos.x+or:pos.x,
						cy = d==1?pos.y+or:pos.y,
						cz = d==2?pos.z+or:pos.z;
					
					ClientCell c2 = ClientCellBunch.cellBunch.getCell(cx, cy, cz);
					if(c2!=null&&c2.solid==ServerCell.CELL_NOT_COMPRESSED){
						for(int ix=0;ix<Constants.CELL_SIZE;ix++)
							for(int iy=0;iy<Constants.CELL_SIZE;iy++)
								for(int iz=0;iz<Constants.CELL_SIZE;iz++){
									boolean visible = false;
									
									if(!translucent(getValue(ix,iy,iz))){
										int bx = d==0?ix+or:ix,
										    by = d==1?iy+or:iy,
										    bz = d==2?iz+or:iz;
										
										if(contains(pos.x+bx, pos.y+by, pos.z+bz)){
											visible = isTranslucent(bx, by, bz);
										}else{
											ClientCell c = ClientCellBunch.cellBunch.getCellSafe(pos.x + bx, pos.y + by, pos.z + bz);
											if(c==null)
												visible = Constants.RENDER_CELL_BORDERS;
											else
												visible = c.isTranslucent(pos.x + bx - c.pos.x, pos.y + by - c.pos.y, pos.z + bz - c.pos.z);
										}
									}
									
									if(visible)visibleSurfaces[id]++;
								}
					}else if(c2!=null&&c2.cellTranslucent()){//translucent
						visibleSurfaces[id] = Constants.CELL_SIZE2; 
					}else{//solid and not translucent
						visibleSurfaces[id] = 0;
					}
				}
			}
		}else{
			for(int d = 0; d < 3; d++){
				for(int o = 0; o<2; o++){
					int id = d*2+o;
					int or = o*2-1;
					visibleSurfaces[id] = 0;
					
					for(int ix=0;ix<Constants.CELL_SIZE;ix++)
						for(int iy=0;iy<Constants.CELL_SIZE;iy++)
							for(int iz=0;iz<Constants.CELL_SIZE;iz++){
								boolean visible = false;
								
								if(!translucent(getValue(ix,iy,iz))){
									int bx = d==0?ix+or:ix,
									    by = d==1?iy+or:iy,
									    bz = d==2?iz+or:iz;
									
									if(contains(pos.x+bx, pos.y+by, pos.z+bz)){
										visible = isTranslucent(bx, by, bz);
									}else{
										ClientCell c = ClientCellBunch.cellBunch.getCellSafe(pos.x + bx, pos.y + by, pos.z + bz);
										if(c==null)
											visible = Constants.RENDER_CELL_BORDERS;
										else
											visible = c.isTranslucent(pos.x + bx - c.pos.x, pos.y + by - c.pos.y, pos.z + bz - c.pos.z);
									}
								}
								
								if(visible)visibleSurfaces[id]++;
							}
				}
			}
		}
	}
	
	public float blockLightValue(int x, int y, int z){
		byte b = ClientCellBunch.cellBunch.getContainingBlock(x, y, z);
		
		if(b<Block.AIR_DARKNESS){
			return (Block.AIR_DARKNESS-b)/(float)(Block.AIR_COUNT-1);
		}else{
			return 0;
		}
	}
	
	public static final float zeroLight = (float) Math.exp(-2);
	public float lightAt(int x, int y, int z){
		float ret=0f, f;
		
		f=blockLightValue(x-1,y+1,z-1);
		if(f>ret)
			ret = f;
		f=blockLightValue(x-1,y+1,z);
		if(f>ret)
			ret = f;
		f=blockLightValue(x-1,y,z-1);
		if(f>ret)
			ret = f;
		f=blockLightValue(x-1,y,z);
		if(f>ret)
			ret = f;
		f=blockLightValue(x,y+1,z-1);
		if(f>ret)
			ret = f;
		f=blockLightValue(x,y+1,z);
		if(f>ret)
			ret = f;
		f=blockLightValue(x,y,z-1);
		if(f>ret)
			ret = f;
		f=blockLightValue(x,y,z);
		if(f>ret)
			ret = f;
		/*
		f=blockLightValue(x+1,y+1,z+1);
		if(f>ret)
			ret = f;
		f=blockLightValue(x+1,y+1,z);
		if(f>ret)
			ret = f;
		f=blockLightValue(x+1,y,z+1);
		if(f>ret)
			ret = f;
		f=blockLightValue(x+1,y,z);
		if(f>ret)
			ret = f;
		f=blockLightValue(x,y+1,z+1);
		if(f>ret)
			ret = f;
		f=blockLightValue(x,y+1,z);
		if(f>ret)
			ret = f;
		f=blockLightValue(x,y,z+1);
		if(f>ret)
			ret = f;
		f=blockLightValue(x,y,z);
		if(f>ret)
			ret = f;*/
		
		return (float) Math.exp(-2*(1-ret))-zeroLight;
	}
	
	public void loadBuffers(){
		int[] v = new int[3];
		
		for(int d = 0; d < 3; d++){
			for(int o = 0; o<2; o++){
				int id = d*2+o;
				int or = o*2-1;
				
				if(loadRequested[id]){
					visibleSurfaces[id] = 0;
					
					if(arbVertex[id]!=0){
						int av = arbVertex[id];
						int ai = arbIndex[id];
						arbVertex[id] = 0;
						arbIndex[id] = 0;
						glDeleteBuffers(av);
						glDeleteBuffers(ai);
					}
					
					//System.out.println("loading "+this+" dim"+id);
					
					//perform surface visibility Test
					for(int ix=0;ix<Constants.CELL_SIZE;ix++)
						for(int iy=0;iy<Constants.CELL_SIZE;iy++)
							for(int iz=0;iz<Constants.CELL_SIZE;iz++){
								boolean visible = false;
								
								if(!translucent(getValue(ix,iy,iz))){
									int bx = d==0?ix+or:ix,
									    by = d==1?iy+or:iy,
									    bz = d==2?iz+or:iz;
									
									if(contains(pos.x+bx, pos.y+by, pos.z+bz)){
										visible = isTranslucent(bx, by, bz);
									}else{
										ClientCell c = ClientCellBunch.cellBunch.getCellSafe(pos.x + bx, pos.y + by, pos.z + bz);
										if(c==null)
											visible = Constants.RENDER_CELL_BORDERS;
										else
											visible = c.isTranslucent(pos.x + bx - c.pos.x, pos.y + by - c.pos.y, pos.z + bz - c.pos.z);
									}
								}
								
								if(visible)visibleSurfaces[id]++;
								visibilityTest[ix][iy][iz] = visible;
							}
					
					//System.out.println("visible surfaces: "+visibleSurfaces[id]);
	
					if(visibleSurfaces[id]!=0){
						int av= glGenBuffers(), ai= glGenBuffers();
						
						//Initialize Vertex Buffer
						int bufferSize = visibleSurfaces[id]*4*vertexFloatCount*4;
						glBindBuffer(GL_ARRAY_BUFFER, av);
						glBufferData(GL_ARRAY_BUFFER, bufferSize, GL_STATIC_DRAW);
						ByteBuffer buffer = glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY, bufferSize, null);
						
						for(int ix=0;ix<Constants.CELL_SIZE;ix++)
							for(int iy=0;iy<Constants.CELL_SIZE;iy++)
								for(int iz=0;iz<Constants.CELL_SIZE;iz++){
									if(visibilityTest[ix][iy][iz]){
										v[0] = pos.x + ix;
										v[1] = pos.y + iy;
										v[2] = pos.z + iz;
										v[d]+= o;
										//System.out.println("next quad:");
										
										int t=0;
										
										for(int dir = 1;dir >=-1;dir -=2)
											for(int d2=0;d2<3;d2++){
												if(d2!=d){
													v[d2]+=dir;
													buffer.putFloat(v[0]);
													buffer.putFloat(v[1]);
													buffer.putFloat(v[2]);
													
													buffer.putFloat(lightAt(v[0],v[1],v[2]));
													//System.out.println(v[0]+" "+v[1]+" "+v[2]);
													
													buffer.putFloat(d==0?or:0);
													buffer.putFloat(d==1?or:0);
													buffer.putFloat(d==2?or:0);
													
													ClientCellBunch.blockTexture.addTextureCoordinate(getValue(ix,iy,iz), t++, d, o, buffer);
												}
											}
									}
								}
						
						buffer.flip();
						glUnmapBuffer(GL_ARRAY_BUFFER);
						glBindBuffer(GL_ARRAY_BUFFER, 0);
						
						//Initialize Index Buffer
						bufferSize = visibleSurfaces[id]*4*4;
						glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ai);
						glBufferData(GL_ELEMENT_ARRAY_BUFFER, bufferSize, GL_STATIC_DRAW);
						buffer = glMapBuffer(GL_ELEMENT_ARRAY_BUFFER, GL_WRITE_ONLY, bufferSize, null);
						
						for(int i=0;i<visibleSurfaces[id]*4;i++){
							buffer.putInt(i);
						}
						
						buffer.flip();
						glUnmapBuffer(GL_ELEMENT_ARRAY_BUFFER);
						glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
						
						arbVertex[id] = av;
						arbIndex[id] = ai;
					}
					
					loadRequested[id] = false;
				}
			}
		}
	}
	
	private boolean isNeighborBlockingSight(int rx, int ry, int rz){
		ClientCell cell = ClientCellBunch.cellBunch.getCell(rx*Constants.CELL_SIZE+pos.x, ry*Constants.CELL_SIZE+pos.y, rz*Constants.CELL_SIZE+pos.z);
		
		return cell==null || cell.isTranslucencyImpossible();
	}
	
	private void cellVisibilityTest(){
		visibilityImpossible = cellTranslucent()||(
				isNeighborBlockingSight(-1,0,0)&&
				isNeighborBlockingSight(1,0,0)&&
				isNeighborBlockingSight(0,-1,0)&&
				isNeighborBlockingSight(0,1,0)&&
				isNeighborBlockingSight(0,0,-1)&&
				isNeighborBlockingSight(0,0,1));
		
		if(visibilityImpossible)
			compress();
		
		cellVisibilityTestRequested = false;
	}
	
	public void render(EngineObject observer) {
		if(cellVisibilityTestRequested)
			cellVisibilityTest();
		
		if(visibilityImpossible)
			return;
		
		notRenderedFrames = 0;
		for(int d = 0; d < 3; d++){
			for(int o = 0; o<2; o++){
				int i = d*2+o;
				
				if(visibleSurfaces==null){
					calculateVisibleSurfacesOnNextUpdate = true;
				}else{
					if(visibleSurfaces[i]!=0){
						int or = o*2-1;
						boolean visible;
						
						switch(d){
						case 0:
							visible = ((pos.x + Constants.CELL_SIZE_HALF)-observer.x)/or < Constants.CELL_SIZE_HALF;
							break;
						case 1:
							visible = ((pos.y + Constants.CELL_SIZE_HALF)-(observer.y+((Player)observer).eyeHeight))/or < Constants.CELL_SIZE_HALF;
							break;
						case 2:
							visible = ((pos.z + Constants.CELL_SIZE_HALF)-observer.z)/or < Constants.CELL_SIZE_HALF;
							break;
						default:
								throw new RuntimeException("fehler!");
						}
						
						if(visible){
							if(arbVertex[i]==0){
								loadOnNextUpdate = true;
								loadRequested[i] = true;
							}else{
								glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, arbIndex[i]);
								glBindBuffer(GL_ARRAY_BUFFER, arbVertex[i]);
								
								glVertexPointer(4, GL_FLOAT, vertexFloatCount*4, 0);
								glNormalPointer(GL_FLOAT, vertexFloatCount*4, 4*4);
								glTexCoordPointer(2, GL_FLOAT, vertexFloatCount*4, 7*4);
								
								glDrawElements(GL_QUADS,4*visibleSurfaces[i],GL_UNSIGNED_INT,0);
							}
						}
					}
				}
			}
		}
	}

	public void notifyYourNeighborsCellContentChanged(){
		cellVisibilityTestRequested = true;
		cellContentChanged = true;
	}
	
	public void notifyNeighborOfCellChange(int irx, int iry, int irz){
		ClientCell cell = ClientCellBunch.cellBunch.getCell(irx*Constants.CELL_SIZE+pos.x, iry*Constants.CELL_SIZE+pos.y, irz*Constants.CELL_SIZE+pos.z);
		
		if(cell!=null)
			cell.notifyYourNeighborsCellContentChanged();
	}
	
	public void notifyCellContentChanged(){
		cellContentChanged = true;
		notifyNeighborOfCellChange(0,0,1);
		notifyNeighborOfCellChange(0,0,-1);
		notifyNeighborOfCellChange(0,1,0);
		notifyNeighborOfCellChange(0,-1,0);
		notifyNeighborOfCellChange(1,0,0);
		notifyNeighborOfCellChange(-1,0,0);
	}
	
	public void update(float dt) {
	}
	
	public void updateBuffers(){
		if(visibilityImpossible)
			return;
		
		if(notRenderedFrames++>1000){
			compress();
			notRenderedFrames=0;
		}
		
		if(calculateVisibleSurfacesOnNextUpdate){
			calculateVisibleSurfacesCount();
			ClientCellBunch.renderLoopLoad += 1.1f;
			calculateVisibleSurfacesOnNextUpdate = false;
		}
		
		if(visibleSurfaces!=null){
			if(cellContentChanged){
				calculateVisibleSurfacesCount();
				ClientCellBunch.renderLoopLoad += 1.1f;
				
				//reload loaded dims
				for(int i=0;i<6;i++){
					if(arbVertex[i]!=0){//loaded
						loadRequested[i] = true;
						loadOnNextUpdate = true;
					}
				}
				
				cellContentChanged = false;
			}
			
			if(loadOnNextUpdate){
				loadOnNextUpdate = false;
				loadBuffers();
				ClientCellBunch.renderLoopLoad += 1.1f;
			}
		}
	}
	
	@Override
	public String toString(){
		return pos.toString();
	}
	
	public boolean containsBlockInRange(EngineObject o, float maxDistance){
		float s = Constants.CELL_HALF_DIAGONAL + maxDistance;
		
		float dx = pos.x + Constants.CELL_SIZE_HALF - o.x;
		dx *= dx;
		float dy = pos.y + Constants.CELL_SIZE_HALF - o.y;
		dy *= dy;
		float dz = pos.z + Constants.CELL_SIZE_HALF - o.z;
		dz *= dz;
		
		return (dx+dy+dz) < s*s;
	}
	
	public boolean isBlockInRange(EngineObject o, float maxDistance, int bx, int by, int bz){
		float s = Constants.BLOCK_HALF_DIAGONAL + maxDistance;
		
		float dx = (pos.x + bx + 0.5f)-o.x;
		dx *= dx;
		float dy = (pos.y + by + 0.5f)-o.y;
		dy *= dy;
		float dz = (pos.z + bz + 0.5f)-o.z;
		dz *= dz;
		
		return (dx+dy+dz) < s*s;
	}

	public void compress() {
		for(int i=0;i<6;i++){
			if(arbVertex[i]!=0){
				//System.out.println("Unloading "+this+" dim"+i);
				glDeleteBuffers(arbVertex[i]);
				glDeleteBuffers(arbIndex[i]);
				arbVertex[i] = 0;
				arbIndex[i] = 0;
			}
		}
	}
	
	public boolean isVisibilityImpossible(){
		return visibilityImpossible;
	}
	
	public int countPendingLoads(){
		int ret=0;
		
		if(visibilityImpossible)
			return 0;
		
		if(cellContentChanged)
			return 6;
		
		for(int i=0;i<6;i++)
			if(loadRequested[i])
				ret++;
		
		return ret;
	}
	
	@Override
	public void finalize() throws Throwable{
		try{
			for(int i=0;i<6;i++){
				if(arbVertex[i]!=0){
					System.err.println("WARNING: finalizer detected memoryleak");
					//System.out.println("Unloading "+this+" dim"+i);
					glDeleteBuffers(arbVertex[i]);
					glDeleteBuffers(arbIndex[i]);
					arbVertex[i] = 0;
					arbIndex[i] = 0;
				}
			}
		}catch(Exception e){
			e.printStackTrace();
		}finally{
			super.finalize();
		}
	}
}
